package com.genymobile.scrcpy.five;

import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Log;
import android.view.InputEvent;
import android.view.KeyEvent;
import android.view.MotionEvent;

import com.genymobile.scrcpy.five.cmd.CmdParser;
import com.genymobile.scrcpy.five.cmd.FiveKeyEventCmd;
import com.genymobile.scrcpy.five.cmd.FiveMotionEventCmd;
import com.genymobile.scrcpy.five.cmd.ICmd;
import com.genymobile.scrcpy.five.config.EventMapFactory;
import com.genymobile.scrcpy.five.connection.ConnectionClientForRecord;
import com.genymobile.scrcpy.five.connection.ConnectionConfig;
import com.genymobile.scrcpy.five.connection.ConnectionServer;
import com.genymobile.scrcpy.five.connection.HttpServer;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

/**
 * 事件录制server
 * 事件原来：1. scrcpy注入的事件
 *         2. getevent -l获取到的输入设备上报的事件
 * 以及window端通过fifo发来的命令和comment请求
 */
public class FiveInputEventRecordServer {
    private static final String TAG = FiveInputEventRecordServer.class.getSimpleName();
    private static final boolean USER_RECORD_V2 = true;

    private static final boolean LOG_ENABLE = GlobalConfig.LOG_ENABLE;
    public static boolean RECORD_EVENT = false;
    public static long MAX_SLEEP_INTERVAL = 0;

    private static volatile FiveInputEventRecordServer sInstance;

    public static FiveInputEventRecordServer getsInstance() {
        if (sInstance == null) {
            synchronized (FiveInputEventRecordServer.class) {
                if (sInstance == null) {
                    sInstance = new FiveInputEventRecordServer();
                }
            }
        }
        return sInstance;
    }

    private int displayId = -1;
    public void setDisplayId(int displayId) {
        this.displayId = displayId;
    }

    class DisplayCmds {
        private final Map<Integer, FiveMotionEventCmd> cmdMaps = new HashMap<>();
        private FiveMotionEventCmd current;
        private final int displayId;
        DisplayCmds(int displayId) {
            this.displayId = displayId;
        }

        private void reset() {
            for (FiveMotionEventCmd cmd : cmdMaps.values()) {
                cmd.action = MotionEvent.ACTION_DOWN;
                cmd.downReported = false;
            }
        }

        FiveMotionEventCmd getCmdByFingerId(int fingerIndex) {
            FiveMotionEventCmd ret = cmdMaps.get(fingerIndex);
            if (ret == null) {
                ret = new FiveMotionEventCmd();
                ret.displayId = displayId;
                ret.fingerId = fingerIndex;
                cmdMaps.put(fingerIndex, ret);
            }
            return ret;
        }

        public FiveMotionEventCmd getCurrent() {
            return current;
        }

        public void setCurrent(FiveMotionEventCmd current) {
            this.current = current;
        }

        public int getDisplayId() {
            return displayId;
        }
    }

    private void runGetevent() {
        try {
            // 执行 `getevent` 命令
            Process process = USER_RECORD_V2 ? Runtime.getRuntime().exec("getevent -l") : Runtime.getRuntime().exec("getevent");

            // 获取输出流
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));

            String line;
            System.out.println("Listening for events...");

            Map<Integer, DisplayCmds> displayCmdsHashMap = new HashMap<>();
            StringArrUtils arrUtils = new StringArrUtils();
            // 逐行读取输出
            while (true) {
                line = reader.readLine();
                // 处理输出行
                if (line == null) {
                    Thread.sleep(10);
                    continue;
                }
                if (line.contains("/dev/input/") && !line.contains("add device")) {
                    if (LOG_ENABLE) Log.d(TAG, "getevent=>line=" + line);
                    if (USER_RECORD_V2) {
                        arrUtils.set(line.split(" "));
                        String node = arrUtils.next().replace(":", "");
                        String event = arrUtils.next();
                        String type = arrUtils.next();
                        String valueStr = arrUtils.next();
                        int value = -1;
                        try {
                            value = (int )Long.parseLong(valueStr, 16);  // FFFFFFF ==> -1
                        } catch (Exception e) {}

                        int displayId = EventMapFactory.getEventMap().eventNode2DisplayId(node);
                        if (displayId < 0) {
                            Log.e(TAG, "failed to get display of node: " + node);
                            return;
                        }
                        Log.d(TAG, String.format("displayId=%s node=%s type=%s value=%s ==> %s", displayId, node, type, value, valueStr));

                        DisplayCmds displayCmds = displayCmdsHashMap.get(displayId);
                        if (displayCmds == null) {
                            displayCmds = new DisplayCmds(displayId);
                            displayCmdsHashMap.put(displayId, displayCmds);
                        }
                        FiveMotionEventCmd cmd = displayCmds.getCurrent();
                        // handle
                        if ("DOWN".equalsIgnoreCase(valueStr)) {
                            // down
                            displayCmds.reset();
                            cmd = displayCmds.getCmdByFingerId(0); // down一定是手指0
                            cmd.action = MotionEvent.ACTION_DOWN;
                            displayCmds.setCurrent(cmd);
                        } else if ("UP".equalsIgnoreCase(valueStr)) {
                            // up
                            cmd.action = MotionEvent.ACTION_UP;
                        } else if ("ABS_MT_POSITION_X".equalsIgnoreCase(type)) {
                            // x
                            cmd.x = value;
                        } else if ("ABS_MT_POSITION_Y".equalsIgnoreCase(type)) {
                            // y
                            cmd.y = value;
                        } else if ("ABS_MT_SLOT".equalsIgnoreCase(type)) {
                            // 多指，表名第几根手指
                            cmd = displayCmds.getCmdByFingerId(value);  // 切换手指
                            displayCmds.setCurrent(cmd);
                        } else if ("SYN_REPORT".equalsIgnoreCase(type)) {
                            // 同步上报
                            if (cmd.action == MotionEvent.ACTION_DOWN && cmd.downReported) {
                                // 当前是down，并且down已经上报过了，后面的就变成move
                                cmd.action = MotionEvent.ACTION_MOVE;
                            }
                            cmd.timestampMs = SystemClock.elapsedRealtime();
                            String keymark = null;
                            int fingerId = cmd.fingerId;
                            switch (cmd.action) {
                                case MotionEvent.ACTION_DOWN:
                                    cmd.downReported = true;
                                    keymark = "DOWN_" + fingerId;
                                    break;
                                case MotionEvent.ACTION_MOVE:
                                    keymark = "MOVE_" + fingerId;
                                    break;
                                case MotionEvent.ACTION_UP:
                                    // 只有一个up，所有的手指都要发送一次up
                                    for (Map.Entry<Integer, FiveMotionEventCmd> entry : displayCmds.cmdMaps.entrySet()) {
                                        keymark = "UP_" + entry.getValue().fingerId;
                                        cmd = entry.getValue();
                                        cmd.action = MotionEvent.ACTION_UP;
                                        cmd.timestampMs = SystemClock.elapsedRealtime();
                                        LineRecorder.getInstance().recordNewLineV2(cmd.timestampMs, cmd.toString(), keymark);
                                    }
                                    continue;
                            }
                            LineRecorder.getInstance().recordNewLineV2(cmd.timestampMs, cmd.toString(), keymark);
                        }
                    } else {
                        String[] arr = line.split(" ");
                        String node = arr[0].replace(":", "");
                        int event = Integer.parseInt(arr[1], 16);
                        int type = Integer.parseInt(arr[2], 16);
                        String value = arr[3];
                        if (!value.equalsIgnoreCase("DOWN") && !value.equalsIgnoreCase("UP")) {
                            value = String.valueOf((int) Long.parseLong(value, 16));
                        }
                        String newLine = String.format("sendevent %s %s %s %s", node, event, type, value);
                        if (LOG_ENABLE) Log.d(TAG, "getevent=>" + newLine);
                        LineRecorder.getInstance().recordNewLine(SystemClock.elapsedRealtime(), newLine);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        Log.e(TAG, "runGetevent() end");
    }

    public static class LineRecorder {
        private static LineRecorder instance;
        public static LineRecorder getInstance() {
            if (instance == null) {
                synchronized (LineRecorder.class) {
                    if (instance == null) {
                        instance = new LineRecorder();
                    }
                }
            }
            return instance;
        }

        private final FileWriter writer;
        public static final File outputFile = new File("/data/local/tmp/record.sh");
        private LineRecorder() {
            outputFile.delete();
            try {
                outputFile.createNewFile();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            try {
                writer = new FileWriter(outputFile);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        private long mLastRecordTime = -1;
        private final static long THRESHOLD = 10;
        private int mForceNextSleepMs = -1;
        private volatile boolean mKeyUpReceived = false;
        private volatile boolean finished = false;
        synchronized void recordNewLine(long timestampMs, String line) {
            if (LOG_ENABLE) Log.e(TAG, "LINE===>" + line);
            line = line.strip();
            if (TextUtils.isEmpty(line) || finished) {
                return;
            }
            if (line.contains("five_commment")) {
                try {
                    JSONObject json = new JSONObject(line);
                    String comment = json.getString("five_commment");
                    int delay = json.getInt("delay");
                    if (delay > 0) {
                        mForceNextSleepMs = delay;
                    }
                    try {
                        writer.append("########## comment:" + comment + "\n");
                        writer.flush();
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                } catch (JSONException e) {
                    throw new RuntimeException(e);
                } finally {
                    return;
                }
            }

            String cmd = "";

            String keyMark = null;
            if (line.contains(" 1 330 0")) {
                // key up
                mKeyUpReceived = true;
            } else if (line.contains(" 0 0 0")) {
                // sync
                if (mKeyUpReceived) {
                    keyMark = "UP";
                    mKeyUpReceived = false;
                }
            } else if (line.contains(" 1 330 1")) {
                // key down
                keyMark = "DOWN";
            } else if (line.contains(" 1 111 1")) {
                // ESC
                finished = true;
            }

            long now = timestampMs;
            if (mLastRecordTime > 0) {
                float derta = now - mLastRecordTime;
                if (mForceNextSleepMs > 0) {
                    derta = mForceNextSleepMs;
                    mForceNextSleepMs = 0;
                }
                if (derta >= THRESHOLD) {
                    if (derta > 1000) {
                        cmd += "########## END\n\n";
                    }
                    cmd += "sleep " + derta/1000 + "; ";
                }
            }
            mLastRecordTime = now;
            if (keyMark != null) {
                line += " #### " + keyMark;
            }
            try {
                writer.append(cmd + line + "\n");
                writer.flush();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        synchronized void recordNewLineV2(long timestampMs, String line, String keyMark) {
            if (GlobalConfig.LOG_ENABLE)    LogUtils.loge(TAG, "recordNewLineV2(): " + line);
            line = line.strip();
            if (TextUtils.isEmpty(line) || finished) {
                return;
            }
            if (line.contains("five_commment")) {
                LogUtils.log(TAG, "recordNewLineV2() ==> " + line);
                try {
                    JSONObject json = new JSONObject(line);
                    String comment = json.getString("five_commment");
                    int delay = json.getInt("delay");
                    if (delay > 0) {
                        mForceNextSleepMs = delay;
                    }
                    try {
                        writer.append("########## comment:" + comment + "\n");
                        writer.flush();
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                } catch (JSONException e) {
                    throw new RuntimeException(e);
                } finally {
                    return;
                }
            }

            String cmd = "";

            if (mLastRecordTime > 0) {
                long derta = timestampMs - mLastRecordTime;
                if (mForceNextSleepMs > 0) {
                    derta = mForceNextSleepMs;
                    mForceNextSleepMs = 0;
                }
                if (derta >= THRESHOLD) {
                    if (derta > 1000) {
                        cmd += "########## END\n\n";
                    }
                    if (derta > MAX_SLEEP_INTERVAL) {
                        derta = MAX_SLEEP_INTERVAL;  // 最大间隔
                    }
                    cmd += "000 sleep " + derta + "\n";
                }
            }
            mLastRecordTime = timestampMs;
            if (keyMark != null) {
                line += " #### " + keyMark;
            }
            try {
                writer.append(cmd + line + "\n");
                writer.flush();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public void startGeteventThread() {
        LogUtils.log(TAG, "startGeteventThread()");
        new Thread() {
            @Override
            public void run() {
                runGetevent();
            }
        }.start();
    }

    /**
     * server，接收真实scrcpy记录的输入事件，和来着window端的命令
     */
    public void startRecordThread(boolean write2file) {
        LogUtils.log(TAG, "startRecordThread() write2file=" + write2file);
        new ConnectionServer("recordServer", ConnectionConfig.PORT_FOR_RECORD_SERVER, new FinalValueWriter(write2file)).start();
        try {
            new HttpServer(ConnectionConfig.PORT_FOR_RECORD_HTTP_SERVER, new RecordCmdHandler()).start();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public synchronized void record(InputEvent inputEvent, int displayId, int injectMode) {
        if (inputEvent == null) {
            return;
        }
        if (USER_RECORD_V2) {
            recordV2(inputEvent, displayId, injectMode);
        }
    }

    public synchronized void recordV2(InputEvent inputEvent, int displayId, int injectMode) {
        long now = SystemClock.elapsedRealtime();
        if (inputEvent instanceof MotionEvent) {
            MotionEvent e = (MotionEvent) inputEvent;
            String cmd = FiveMotionEventCmd.convertMotionEventToOneEvent(now, e, displayId).toString();
            if (LOG_ENABLE) Log.d(TAG, "recordV2() cmd=" + cmd);
            ConnectionClientForRecord.send(cmd);
        } else if (inputEvent instanceof KeyEvent) {
            KeyEvent keyEvent = (KeyEvent) inputEvent;
            if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_ESCAPE) {
                // 不记录esc键，这个是退出录制的快捷键
                return;
            }
            String cmd = FiveKeyEventCmd.convert(keyEvent, displayId).toString();
            ConnectionClientForRecord.send(cmd);
        }
    }

    private static class RecordCmdHandler implements ConnectionServer.ICallback {
        private final Map<String, Function<String, Boolean>> mCmdMaps = new HashMap<>();
        RecordCmdHandler() {
            mCmdMaps.put("comment", this::handleCmdComment);
        }

        Boolean handleCmdComment(String value) {
//            LogUtils.log(TAG, "handleCmdComment() " + value);
            ConnectionClientForRecord.send(value);
            return null;
        }

        @Override
        public void handleLine(String line) {
            LogUtils.log(TAG, "handleCmd:" + line);
            try {
                JSONObject obj = new JSONObject(line);
                String cmd = obj.getString("cmd");
                String value = obj.getString("value");
                if (TextUtils.isEmpty(cmd) || TextUtils.isEmpty(value)) {
                    LogUtils.loge(TAG, "cmd or value is empty!!! cmd=" + cmd + " value=" + value);
                    return;
                }
                for (Map.Entry<String, Function<String, Boolean>> e : mCmdMaps.entrySet()) {
                    if (cmd.equals(e.getKey())) {
                        e.getValue().apply(value);
                        return;
                    }
                }
                LogUtils.log(TAG, "unknown cmd" + cmd);
            } catch (JSONException e) {
                LogUtils.loge(TAG, "e=" + e);
            }
        }
    }

    private static class FinalValueWriter implements ConnectionServer.ICallback {
        private long mLastTimestampMs;
        private final boolean write2file;
        FinalValueWriter(boolean write2file) {
            this.write2file = write2file;
        }

        @Override
        public void handleLine(String line) {
            if (LOG_ENABLE) Log.d(TAG, "readFromFifo line=" + line);

            if (!write2file) {
                if (LOG_ENABLE) Log.d(TAG, "no need to write to file");
                return;
            }

            if (USER_RECORD_V2) {
                ICmd cmd  = CmdParser.parse(line);
                long timestamp = cmd != null ? cmd.timestampMs : 0;
                LineRecorder.getInstance().recordNewLineV2(timestamp, line, null);
            } else {
                if (line.contains("time:")) {
                    String[] tmp = line.split(" ");
                    mLastTimestampMs = Long.parseLong(tmp[1].trim());
                    return;
                }
                LineRecorder.getInstance().recordNewLine(mLastTimestampMs, line);
            }
        }

        @Override
        public void onServerFinished() {
            System.exit(0);
        }
    }
}
